/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package k3tool.cli;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import k3tool.common.filter.FileName2Toc;
import k3tool.common.filter.Html2XHtml;
import k3tool.common.filter.LocalDirectory;
import k3tool.common.filter.Txt2XHtml;
import k3tool.common.filter.XHtmlConstants;
import k3tool.opf.model.KindleMetaData;
import k3tool.opf.model.KindleOpfConstants;
import k3tool.opf.model.KindleResourceType;
import k3tool.opf.model.KindleResourceTypeHelper;
import k3tool.opf.model.KindleTableOfContent;
import k3tool.opf.model.TableOfContentItem;
import k3tool.opf.model.TableOfContentItemHelper;
import nu.xom.Attribute;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.Nodes;
import nu.xom.Serializer;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.apache.commons.io.FileUtils;

/**
 *
 * @author Jeff Huang
 */
public class SimpleTxtFileProcessor {

    private static final Logger logger = Logger.getLogger(SimpleTxtFileProcessor.class.getName());

    public static Options buildOptions() {
        Options opts = new Options();

        opts.addOption(OptionBuilder.withArgName("file").hasArg().withDescription("Create configuration template").create('C'));
        opts.addOption(OptionBuilder.withArgName("baseDir").hasArg().withDescription("Base input directory").create('b'));
        opts.addOption(OptionBuilder.withArgName("targetDir").hasArg().withDescription("Target output directory").create('o'));
        opts.addOption(OptionBuilder.withArgName("file").hasArg().withDescription("Configuration file").create('c'));
        return opts;
    }

    public static void usage(Options opts) {
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp("java -cp <classpath> " + SimpleTxtFileProcessor.class.getName(),
                opts);
    }

    public static void main(String[] args) throws Exception {
        CommandLineParser parser = new PosixParser();
        Options options = buildOptions();

        try {
            CommandLine line = parser.parse(options, args);
            if (line.hasOption('C')) {
                buildConfigTemplate(new File(line.getOptionValue('C')));
                return;
            }
            if (!line.hasOption('b')) {
                System.out.println("Missing base dir\n");
                usage(options);
                return;
            }
            if (!line.hasOption('o')) {
                System.out.println("Missing target dir\n");
                usage(options);
                return;
            }
            if (!line.hasOption('c')) {
                System.out.println("Missing configuration file\n");
                usage(options);
                return;
            }

            String baseDir = line.getOptionValue('b');
            String targetDir = line.getOptionValue('o');
            String configFile = line.getOptionValue('c');

            SimpleTxtFileProcessor processor = new SimpleTxtFileProcessor(new File(baseDir), new File(targetDir), new File(configFile));
            processor.init();
            processor.process();


        } catch (ParseException exp) {
            System.out.println("Unexpected exception:" + exp.getMessage());
        }


    }

    private static void buildConfigTemplate(File file) throws IOException {
        KindleMetaData metaData = new KindleMetaData();

        metaData.setTitle("Title of the book");
        metaData.setCreator("Name of the creator");
        metaData.setPublisher("Name of the publisher");
        metaData.setLanguage("zh-cn");
        metaData.setSubject("Subject of the book");
        metaData.setInputCharset("GB18030");

        KindleMetaDataConfigHelper.createConfigFile(metaData, file);
    }
    private File base;
    private File target;
    private File configFile;
    private KindleMetaData metaData;

    public SimpleTxtFileProcessor(File base, File target, File configFile) {
        this.base = base;
        this.target = target;
        this.configFile = configFile;
    }

    public void buildTxtTargetContent(File file, File target, String inputCharset) throws IOException {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream(file);
            Reader reader = new InputStreamReader(fis, inputCharset);
            fos = new FileOutputStream(new File(target, normalizeTargetFileName(file.getName())));
            Txt2XHtml txt2XHtml = new Txt2XHtml();
            txt2XHtml.filter(reader, fos, "UTF-8");
            reader.close();
            fis.close();
            fos.close();
            fis = null;
            fos = null;
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException ex) {
                    logger.log(Level.WARNING, file.getAbsolutePath(), ex);
                }
            }
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException ex) {
                    logger.log(Level.WARNING, target.getAbsolutePath(), ex);
                }
            }
        }
    }

    public void init() throws IOException {
        if (!base.exists()) {
            throw new FileNotFoundException(base.getAbsolutePath());
        }

        if (!base.isDirectory()) {
            throw new IllegalArgumentException(base.getAbsolutePath() + " should be a directory");
        }

        if (!target.exists()) {
            target.mkdirs();
        }

        if (!target.isDirectory()) {
            throw new IllegalArgumentException(target.getAbsolutePath() + " is not a valid directory");
        }

        metaData = KindleMetaDataConfigHelper.loadMetaDataFromConfigFile(configFile);
    }

    public File getContentBase() {
        return new File(target, "content");
    }

    public File getOpfFile(){
        File contentDir = getContentBase();
        return new File(contentDir, "Kindle-Book.opf");
    }

    public void process() throws IOException {
        File contentDir = getContentBase();
        contentDir.mkdirs();
        buildTargetContent(base, contentDir, metaData.getInputCharset());

        LocalDirectory baseRootDir = new LocalDirectory(contentDir, contentDir, true);
        List<TableOfContentItem> itemList = baseRootDir.getAsToc();
        KindleTableOfContent toc = new KindleTableOfContent();
        toc.setItemList(itemList);
        metaData.setToc(toc);

        if (metaData.getTocFile() == null) {
            buildTocFile(metaData, contentDir);
        } else {
            copyTocFile(metaData, contentDir);
        }

        Element metaEl = buildMetaElement(metaData);
        Element manifestEl = buildManifest(contentDir);
        Element spineEl = buildSpineElement(contentDir);
        Element guideEl = buildGuideElement(metaData, manifestEl);

        Element root = new Element("package", KindleOpfConstants.OPF_NAMESPACE);
        root.addAttribute(new Attribute("version", "2.0"));
        root.addAttribute(new Attribute("unique-identifier", "BookId"));

        root.appendChild(metaEl);
        root.appendChild(manifestEl);
        root.appendChild(spineEl);
        root.appendChild(guideEl);

        Document doc = new Document(root);

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(getOpfFile());
            Serializer s = new Serializer(fos, "UTF-8");
            s.setIndent(4);
            s.write(doc);
            fos.close();
            fos = null;
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException ex) {
                    //do nothing here
                }
            }
        }
    }

    /**
     * recursively build
     */
    public void buildTargetContent(File base, File target, String inputCharset) throws IOException {
        File[] files = base.listFiles();

        for (File file : files) {
            if (file.isDirectory()) {
                File newTarget = new File(target, file.getName());
                newTarget.mkdirs();
                buildTargetContent(file, newTarget, inputCharset);
            } else {
                if (file.length() > 500 * 1024) { //bigger thank 500 k
                    logger.info("File is too big : " + file.getAbsolutePath() + " / " + file.length());
                    continue;
                }
                /*
                if (!file.getName().toLowerCase().endsWith(".txt")) {
                    logger.info("Copy original file : " + file.getAbsolutePath());
                    FileUtils.copyFile(file, new File(target, file.getName()));
                    continue;
                }*/
                String lowcaseFileName = file.getName().toLowerCase();

                if (lowcaseFileName.endsWith(".txt")) {
                    buildTxtTargetContent(file, target, inputCharset);
                } else if (lowcaseFileName.endsWith(".htm") || lowcaseFileName.endsWith(".html") || lowcaseFileName.endsWith(".xhtml")) {
                    new Html2XHtml().filter(file, XHtmlConstants.getBaseUrl(file), new File(target, normalizeTargetFileName(file.getName())), inputCharset);
                }
            }
        }

    }

    public Element buildMetaElement(KindleMetaData metaData) {
        Element metadataElement = new Element("metadata");
        metadataElement.addNamespaceDeclaration("dc", KindleOpfConstants.DC_NAMESPACE);
        metadataElement.addNamespaceDeclaration("opf", KindleOpfConstants.OPF_NAMESPACE);

        Element titleElement = new Element("dc:title", KindleOpfConstants.DC_NAMESPACE);
        titleElement.appendChild(metaData.getTitle());
        metadataElement.appendChild(titleElement);

        Element langElement = new Element("dc:language", KindleOpfConstants.DC_NAMESPACE);
        langElement.appendChild(metaData.getLanguage());
        metadataElement.appendChild(langElement);

        if (metaData.getCoverPath() != null) {
            String coverResourceId = getItemId(new File(metaData.getCoverPath()));

            if (coverResourceId != null) {
                Element metaElement = new Element("meta");
                metaElement.addAttribute(new Attribute("name", "cover"));
                metaElement.addAttribute(new Attribute("content", coverResourceId));
                metadataElement.appendChild(metaElement);
            }
        }

        Element creatorElement = new Element("dc:creator", KindleOpfConstants.DC_NAMESPACE);
        creatorElement.appendChild(metaData.getCreator());
        metadataElement.appendChild(creatorElement);

        Element subjectElement = new Element("dc:subject", KindleOpfConstants.DC_NAMESPACE);
        subjectElement.appendChild(metaData.getSubject());
        metadataElement.appendChild(subjectElement);

        Element dateElement = new Element("dc:date", KindleOpfConstants.DC_NAMESPACE);
        dateElement.appendChild(KindleOpfConstants.getDateFormatter().format(new Date()));
        metadataElement.appendChild(dateElement);

        Element descElement = new Element("dc:description", KindleOpfConstants.DC_NAMESPACE);
        descElement.appendChild(metaData.getDesc());
        metadataElement.appendChild(descElement);

        return metadataElement;
    }



    private Element buildManifest(File contentDir) {
        Element el = new Element("manifest");
        HashSet<String> idSet = new HashSet<String>();
        Stack<File> processStack = new Stack<File>();
        processStack.push(contentDir);

        while (!processStack.isEmpty()) {
            File cur = processStack.pop();
            if (!cur.isDirectory()) {
                throw new IllegalArgumentException(cur.getAbsolutePath() + " is not a directory");
            }
            File[] files = cur.listFiles(new FileFilter() {

                @Override
                public boolean accept(File pathname) {
                    if (pathname.getName().startsWith(".")) {
                        return false;
                    }
                    return true;
                }
            });

            LinkedList<File> dirList = new LinkedList<File>();
            for (File file : files) {
                if (file.isDirectory()) {
                    dirList.add(file);
                } else {
                    String suffix = file.getName().toLowerCase();
                    int pos = suffix.lastIndexOf(".");
                    if (pos == -1) {
                        continue; //missing suffix
                    }
                    suffix = suffix.substring(pos + 1);
                    KindleResourceType type = KindleResourceTypeHelper.suffixTypeMap.get(suffix);
                    if (type == null) {
                        logger.info("Unsupport type : " + file.getAbsolutePath());
                        continue; //unsupport type
                    }

                    if (idSet.contains(getItemId(file))) {
                        continue;       //duplicated item
                    }
                    Attribute idAttr = new Attribute("id", getItemId(file));
                    Attribute typeAttr = new Attribute("media-type", KindleResourceTypeHelper.typeNameMap.get(type));
                    Attribute hrefAttr = new Attribute("href", FileName2Toc.getRelativePath(file, contentDir));

                    Element itemEl = new Element("item");
                    itemEl.addAttribute(idAttr);
                    itemEl.addAttribute(typeAttr);
                    itemEl.addAttribute(hrefAttr);
                    el.appendChild(itemEl);
                    idSet.add(getItemId(file));
                }
            }
            Collections.reverse(dirList);
            processStack.addAll(dirList);
        }
        return el;
    }

    public Element buildSpineElement(File contentDir) {
        Element spineEl = new Element("spine");

        Stack<File> processStack = new Stack<File>();
        processStack.push(contentDir);

        while (!processStack.isEmpty()) {
            File cur = processStack.pop();
            if (!cur.isDirectory()) {
                throw new IllegalArgumentException(cur.getAbsolutePath() + " is not a directory");
            }
            File[] files = cur.listFiles(new FileFilter() {

                @Override
                public boolean accept(File pathname) {
                    if (pathname.getName().startsWith(".")) {
                        return false;
                    }
                    return true;
                }
            });

            ArrayList<File> fileList = new ArrayList<File>();
            fileList.addAll(Arrays.asList(files));

            Collections.sort(fileList, new Comparator<File>() {

                @Override
                public int compare(File o1, File o2) {
                    return o1.getName().compareTo(o2.getName());
                }
            });


            LinkedList<File> dirList = new LinkedList<File>();
            for (File file : fileList) {
                if (file.isDirectory()) {
                    dirList.add(file);
                } else {

                    if (!KindleResourceTypeHelper.couldBeSpineItem(file))
                        continue;

                    Attribute idAttr = new Attribute("idref", getItemId(file));

                    Element itemEl = new Element("itemref");
                    itemEl.addAttribute(idAttr);
                    spineEl.appendChild(itemEl);
                }

            }
            Collections.reverse(dirList);
            processStack.addAll(dirList);
        }

        return spineEl;
    }

    public Element buildGuideElement(KindleMetaData metaData, Element manifestEl) {
        Element el = new Element("guide");
        if (metaData.getTocFile() != null) {
            String itemId = getItemId(new File(metaData.getTocFile()));
            if (itemId == null) {
                logger.warning("TOC file not found");
            } else {
                String xp = String.format("//item[@id = '%s']/@href", itemId);
                Nodes nodes = manifestEl.query(xp);
                if (nodes.size() == 0) {
                    logger.warning("TOC file not found in the manifest");
                } else {
                    String href = nodes.get(0).getValue();
                    Element refEl = new Element("reference");
                    refEl.addAttribute(new Attribute("type", "toc"));
                    refEl.addAttribute(new Attribute("title", "Table of Contents"));
                    refEl.addAttribute(new Attribute("href", href));
                    el.appendChild(refEl);
                }
            }
        }
        if (metaData.getWelcomePath() != null) {
            String itemId = getItemId(new File(metaData.getWelcomePath()));
            if (itemId == null) {
                logger.warning("Welcome file not found");
            } else {
                String xp = String.format("//item[@id = '%s']/@href", itemId);
                Nodes nodes = manifestEl.query(xp);
                if (nodes.size() == 0) {
                    logger.warning("Welcome file not found in the manifest");
                } else {
                    String href = nodes.get(0).getValue();
                    Element refEl = new Element("reference");
                    refEl.addAttribute(new Attribute("type", "text"));
                    refEl.addAttribute(new Attribute("title", "Welcome"));
                    refEl.addAttribute(new Attribute("href", href));
                    el.appendChild(refEl);
                }
            }
        }
        return el;

    }

    public String getItemId(File file) {
        try {
            return String.format("item-%d", FileUtils.checksumCRC32(file));
        } catch (IOException ex) {
            return null;
        }
    }

    public void buildTocFile(KindleMetaData metaData, File target) throws IOException {
        Document doc = XHtmlConstants.createXHtmlDocument();
        Element htmlEl = doc.getRootElement();
        Element bodyEl = new Element("body");
        htmlEl.appendChild(bodyEl);
        Element ulEl = new Element("ul");
        bodyEl.appendChild(ulEl);

        int itemNum = 0;
        for (TableOfContentItem item : metaData.getToc().getItemList()) {
            Element itemEl = TableOfContentItemHelper.generateXHtmlElement(item, "", ++itemNum);
            ulEl.appendChild(itemEl);
        }

        FileOutputStream fos = null;
        File tocFile = new File(target, "000-toc.html");
        try {
            fos = new FileOutputStream(tocFile);
            Serializer s = new Serializer(fos, "UTF-8");
            s.setIndent(4);
            s.write(doc);
            fos.close();
            fos = null;
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException ex) {
                    //do nothing here
                }
            }
        }

        metaData.setTocFile(tocFile.getAbsolutePath());
    }

    public void copyTocFile(KindleMetaData metaData, File contentDir) throws IOException {
        File file = new File(metaData.getTocFile());

        if (!file.exists()) {
            file = new File(base, metaData.getTocFile());
            if (!file.exists()) {
                throw new IllegalArgumentException(metaData.getTocFile() + " does not exist");
            }
        }

        file = file.getAbsoluteFile();
        File parent = file.getParentFile();
        boolean needCopy = true;
        while (parent != null) {
            if (parent.equals(base)) {
                needCopy = false;
                break;
            }
        }

        if (!needCopy) {
            return;
        }

        String targetFileName = "000-toc.html";
        FileUtils.copyFile(file, new File(contentDir, targetFileName));
    }

    private String normalizeTargetFileName(String name) {
        int pos = name.lastIndexOf(".");
        return name.substring(0, pos) + ".html";
    }

    
}
